﻿//QuotingStrategy.cs
//Copyright (c) 2013 StockSharp LLC, all rights reserved.
//This code module is part of StockSharp library.
//This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3.
//See the file License.txt for the license details.
//More info on: http://stocksharp.com

namespace StockSharp.Algo.Strategies
{
	using System;
	using System.Collections.Generic;
	using System.Linq;

	using Ecng.Collections;
	using Ecng.Common;
	using Ecng.ComponentModel;

	using StockSharp.Logging;
	using StockSharp.BusinessEntities;

	/// <summary>
	/// Базовая стратегия на котирование.
	/// </summary>
	public abstract class QuotingStrategy : Strategy
	{
		private Order _order;
		private Order _reRegisteringOrder;
		private bool _isCanceling;
		private bool _isRegistering;
		private Order _manualReRegisterOrder;
		private DateTime _lastRegisterTime;
		private bool _isReRegistedFailed;

		private IEnumerable<IMarketRule> _notificationRules;

		private readonly object _syncToken = new object();

		/// <summary>
		/// Инициализировать <see cref="QuotingStrategy"/>.
		/// </summary>
		/// <param name="quotingDirection">Направление котирования.</param>
		/// <param name="quotingVolume">Объем, который необходимо скотировать.</param>
		protected QuotingStrategy(OrderDirections quotingDirection, decimal quotingVolume)
		{
			CheckQuotingVolume(quotingVolume);

			_quotingDirection = this.Param("QuotingDirection", quotingDirection);
			_quotingVolume = this.Param("QuotingVolume", quotingVolume);
			_maxReRegisterCount = this.Param("MaxReRegisterCount", int.MaxValue);
			_maxRegisterFailCount = this.Param("MaxRegisterFailCount", 100);
			_timeOut = this.Param<TimeSpan>("TimeOut");
			_registerInterval = this.Param<TimeSpan>("RegisterInterval");
			
			DisposeOnStop = true;
		}

		/// <summary>
		/// Инициализировать <see cref="QuotingStrategy"/>.
		/// </summary>
		/// <param name="order">Заявка, которую необходимо котировать.</param>
		protected QuotingStrategy(Order order)
			: this(order.Direction, order.TransactionId == 0 ? order.Volume : order.Balance)
		{
			if (order == null)
				throw new ArgumentNullException("order");

			var isNew = order.TransactionId == 0;

			// если была передана на котирование уже зарегистрированная заявка,
			// то будем котировать именно её
			if (!isNew)
				_order = order;
		}

		private readonly StrategyParam<OrderDirections> _quotingDirection;

		/// <summary>
		/// Направление котирования.
		/// </summary>
		public OrderDirections QuotingDirection
		{
			get { return _quotingDirection.Value; }
			//private set { _quotingDirection.Value = value; }
		}

		private readonly StrategyParam<decimal> _quotingVolume;

		/// <summary>
		/// Объем, который необходимо скотировать.
		/// </summary>
		public virtual decimal QuotingVolume
		{
			get { return _quotingVolume.Value; }
			set
			{
				if (value == QuotingVolume)
					return;

				this.AddInfoLog("Изменение объема для котирования. Старый объем {0}, новый объем {1}.", QuotingVolume, value);

				CheckQuotingVolume(value);

				if (ProcessState == ProcessStates.Started)
					SyncProcessQuoting();

				_quotingVolume.Value = value;
			}
		}

		private static void CheckQuotingVolume(decimal quotingVolume)
		{
			if (quotingVolume <= 0)
				throw new ArgumentOutOfRangeException("quotingVolume", quotingVolume, "Котируемый должен быть положительным.");

			//if (checkOnZero && quotingVolume == 0)
			//	throw new ArgumentOutOfRangeException("quotingVolume", quotingVolume, "Котируемый объем не может быть нулевым.");
		}

		/// <summary>
		/// Заявки, с которыми работает в данный момент котировщик.
		/// </summary>
		public virtual IEnumerable<Order> QuotingOrders
		{
			get
			{
				return _order == null ? Enumerable.Empty<Order>() : new[] { _order };
			}
		}

		private readonly StrategyParam<int> _maxReRegisterCount;

		/// <summary>
		/// Максимальное количество изменений заявки, при превышении которого алгоритм будет остановлен.
		/// </summary>
		/// <remarks>
		/// Значение по умолчанию равно <see cref="int.MaxValue"/>.
		/// </remarks>
		public int MaxReRegisterCount
		{
			get { return _maxReRegisterCount.Value; }
			set
			{
				if (value < 0)
					throw new ArgumentOutOfRangeException("value", value, "Неправильное значение.");

				_maxReRegisterCount.Value = value;
			}
		}

		/// <summary>
		/// Текущее количество изменений заявки.
		/// </summary>
		public int CurrentReRegisterCount { get; private set; }

		private readonly StrategyParam<int> _maxRegisterFailCount;

		/// <summary>
		/// Максимальное количество ошибок регистрации заявки, при превышении которого алгоритм будет остановлен.
		/// </summary>
		/// <remarks>
		/// Значение по умолчанию равно 100.
		/// </remarks>
		public int MaxRegisterFailCount
		{
			get { return _maxRegisterFailCount.Value; }
			set { _maxRegisterFailCount.Value = value; }
		}

		/// <summary>
		/// Текущее количество ошибок регистрации заявки.
		/// </summary>
		public int CurrentRegisterFailCount { get; private set; }

		/// <summary>
		/// Объем, который осталось реализовать до завершения котирования.
		/// </summary>
		public decimal LeftVolume
		{
			get { return QuotingVolume - Position.Abs(); }
		}

		private readonly StrategyParam<TimeSpan> _timeOut;

		/// <summary>
		/// Ограничение по времени, в течении которого должно отработать котирование.
		/// Если весь объем <see cref="QuotingVolume"/> не будет реализован к этому времени, то стратегия остановит свою работу.
		/// </summary>
		/// <remarks>
		/// По-умолчанию ограничение выключено, и равно <see cref="TimeSpan.Zero"/>.
		/// </remarks>
		public TimeSpan TimeOut
		{
			get { return _timeOut.Value; }
			set
			{
				if (value < TimeSpan.Zero)
					throw new ArgumentOutOfRangeException("value", value, "Ограничение по времени не может быть отрицательным.");

				_timeOut.Value = value;
			}
		}

		/// <summary>
		/// Наступил ли <see cref="TimeOut"/>.
		/// </summary>
		protected bool IsTimeOut
		{
			get
			{
				return (TimeOut != TimeSpan.Zero && (this.GetMarketTime() - StartedTime) >= TimeOut);
			}
		}

		private readonly StrategyParam<TimeSpan> _registerInterval;

		/// <summary>
		/// Интервал частоты ререгистрации заявки, при превышении которого новая заявка выставляться не будет.
		/// </summary>
		/// <remarks>
		/// По-умолчанию интервал выключен, и равен <see cref="TimeSpan.Zero"/>.
		/// </remarks>
		public TimeSpan RegisterInterval
		{
			get { return _registerInterval.Value; }
			set
			{
				if (value < TimeSpan.Zero)
					throw new ArgumentOutOfRangeException("value", value, "Интервал не может быть отрицательным.");

				_registerInterval.Value = value;
			}
		}

		/// <summary>
		/// Можно ли остановить котирование.
		/// </summary>
		/// <remarks>
		/// По умолчанию, котирование останавливается тогда, когда все контракты реализованы и <see cref="LeftVolume"/> равно 0.
		/// </remarks>
		/// <returns>True, можно, иначе, false.</returns>
		protected virtual bool NeedFinish()
		{
			return LeftVolume <= 0;
		}

		/// <summary>
		/// Получить цену для выставляемой заявки.
		/// </summary>
		/// <param name="acceptablePriceRange">Приемлемый диапазон цены.</param>
		/// <returns>Цена, по которой будет выставлена заявка. Если возвращается 0, то заявка выставляться не будет.</returns>
		protected virtual decimal GetOrderPrice(Range<decimal> acceptablePriceRange)
		{
			if (acceptablePriceRange == null)
				throw new ArgumentNullException("acceptablePriceRange");

			return QuotingDirection == OrderDirections.Buy ? acceptablePriceRange.Max : acceptablePriceRange.Min;
		}

		/// <summary>
		/// Нужно ли котировать заявку.
		/// </summary>
		/// <param name="currentPrice">Текущая цена.</param>
		/// <param name="currentVolume">Текущий объем.</param>
		/// <param name="acceptablePriceRange">Приемлемый диапазон цены.</param>
		/// <param name="newVolume">Новый объем.</param>
		/// <returns>Цена, по которой будет выставлена заявка. Если возвращается 0, то заявка выставляться не будет.</returns>
		protected virtual decimal NeedQuoting(decimal currentPrice, decimal currentVolume, Range<decimal> acceptablePriceRange, decimal newVolume)
		{
			if (acceptablePriceRange == null)
				throw new ArgumentNullException("acceptablePriceRange");

			if (!acceptablePriceRange.Contains(currentPrice) || currentVolume != newVolume)
				return GetOrderPrice(acceptablePriceRange);
			else
				return 0;
		}

		/// <summary>
		/// Получить лучшую цену. Если невозможно вычислить лучшую цену на данный момент, то будет возвращено 0.
		/// </summary>
		protected virtual decimal BestPrice
		{
			get
			{
				var quote = FilteredQuotes.FirstOrDefault();

				if (quote != null)
				{
					return quote.Price;
				}
				else
				{
					return (decimal)Security.GetCurrentPrice(QuotingDirection.Invert());
				}
			}
		}

		/// <summary>
		/// Получить приемлемый диапазон цены, попадая в который текущая заявка не будет перерегистрирована.
		/// </summary>
		/// <returns>Приемлемый диапазон цены. Если невозможно вычислить диапазон на данный момент, то будет возвращено null.</returns>
		protected virtual Range<decimal> GetAcceptablePriceRange()
		{
			var price = BestPrice;
			return price == 0 ? null : new Range<decimal>(price, price);
		}

		/// <summary>
		/// Получить новый объем для заявки.
		/// </summary>
		/// <returns>Новый объем для заявки.</returns>
		protected virtual decimal GetNewVolume()
		{
			if (Volume <= 0)
				return LeftVolume;

			var newVolume = Volume;

			if (LeftVolume > 0)
				newVolume = newVolume.Min(LeftVolume);

			return newVolume;
		}

		/// <summary>
		/// Отфильтрованные с помощью <see cref="TraderHelper.GetFilteredQuotes(StockSharp.BusinessEntities.Security,System.Collections.Generic.IEnumerable{StockSharp.BusinessEntities.Order})"/>
		/// котировки стакана (биды или оффера в зависимости от значения свойства <see cref="QuotingDirection"/>).
		/// </summary>
		protected IEnumerable<Quote> FilteredQuotes
		{
			get
			{
				return Security
							.GetMarketDepth()
							.GetQuotes(QuotingDirection)
							.GetFilteredQuotes(QuotingOrders);

				// mika котировки уже передаются в GetFilteredQuotes отсортированными,
				// и их порядок не меняется
				//
				//quotes = QuotingDirection == OrderDirections.Buy
				//            ? quotes.OrderByDescending(q => q.Price)
				//            : quotes.OrderBy(q => q.Price);

				//return quotes.ToArray();
			}
		}

		/// <summary>
		/// Получить список правил, на которые будет реагировать котирование.
		/// </summary>
		/// <returns>Список правил.</returns>
		protected virtual IEnumerable<IMarketRule> GetNotificationRules()
		{
			return new []
			{
				(IMarketRule)Security.WhenMarketDepthChanged()/*,
				Security.WhenChanged()*/
			};
		}

		/// <summary>
		/// Приостановить правила нотификации стратегии котирования и выполнить указанное действие.
		/// </summary>
		/// <param name="action">Действие.</param>
		public void SuspendNotificationRules(Action action)
		{
			_notificationRules.ForEach(r => r.Suspend(true));

			try
			{
				if (ProcessState == ProcessStates.Started)
					action();
				else
					this.AddInfoLog("Действие не выполнено! Ожидается остановка стратегии!");
			}
			finally
			{
				if (ProcessState == ProcessStates.Started)
					_notificationRules.ForEach(r => r.Suspend(false));
			}
		}

		/// <summary>
		/// Метод вызывается тогда, когда состояние процесса <see cref="Strategy.ProcessState"/> перешло в значение <see cref="ProcessStates.Stopped"/>.
		/// </summary>
		protected override void OnStopped()
		{
			base.OnStopped();
			_order = null;
		}

		/// <summary>
		/// Метод вызывается тогда, когда вызвался метод <see cref="Strategy.Start"/>, и состояние <see cref="Strategy.ProcessState"/> перешло в значение <see cref="ProcessStates.Started"/>.
		/// </summary>
		protected override void OnStarted()
		{
			base.OnStarted();

			//CurrentBestPrice = null;
			CurrentReRegisterCount = 0;
			CurrentRegisterFailCount = 0;
			_isCanceling = false;
			_isRegistering = false;
			_reRegisteringOrder = null;
			_manualReRegisterOrder = null;
			_reRegisteringOrder = null;
			_lastRegisterTime = default(DateTime);
			_isReRegistedFailed = false;

			this.AddInfoLog("Котирование на {0} объема {1}.", QuotingDirection, QuotingVolume);

			this.SuspendRules(() =>
			{
				this
					.WhenStopping()
					.Do(() =>
					{
						if (LeftVolume > 0)
							this.AddWarningLog("Заканчиваем котирование с неисполненным объемом равный {0}.", LeftVolume);
					})
					.Once()
					.Sync(_syncToken)
					.Apply(this);

				_notificationRules = GetNotificationRules();
				if (!_notificationRules.IsEmpty())
				{
					_notificationRules
						.Or()
						.Do(ProcessQuoting)
						.Sync(_syncToken)
						.Apply(this);
				}

				this
					.WhenPositionChanged()
					.Do(() =>
					{
						this.AddInfoLog("Позиция изменилась на {0}. Оставшийся объем {1}.", Position, LeftVolume);

						if (NeedFinish())
						{
							this.AddInfoLog("Заканчиваем котирование.");
							Stop();
						}
					})
					.Sync(_syncToken)
					.Apply(this);

				if (TimeOut > TimeSpan.Zero)
				{
					Trader
						.WhenIntervalElapsed(TimeOut)
						.Do(ProcessTimeOut)
						.Once()
						.Apply(this);
				}
			});

			if (!IsRulesSuspended)
				SyncProcessQuoting();
		}

		/// <summary>
		/// Обработчик события наступления <see cref="TimeOut"/>.
		/// </summary>
		protected virtual void ProcessTimeOut()
		{
			Stop();
		}

		/// <summary>
		/// Метод вызывается тогда, когда состояние процесса <see cref="Strategy.ProcessState"/> перешло в значение <see cref="ProcessStates.Stopping"/>.
		/// </summary>
		protected override void OnStopping()
		{
			_notificationRules.ForEach(r => r.Suspend(true));

			base.OnStopping();
		}

		/// <summary>
		/// Инициировать котирование синхронно.
		/// </summary>
		protected void SyncProcessQuoting()
		{
			lock (_syncToken)
				ProcessQuoting();
		}

		/// <summary>
		/// Нужно ли остановить котирование из-за ошибки регистрации новой заявки.
		/// </summary>
		/// <param name="fail">Описание ошибки.</param>
		/// <returns>True, если нужно остановить котирование. Иначе, false.</returns>
		protected virtual bool NeedStop(OrderFail fail)
		{
			CurrentRegisterFailCount++;
			this.AddInfoLog("Текущее кол-во ошибок {0}. Максимальное {1}.", CurrentRegisterFailCount, MaxRegisterFailCount);
			return CurrentRegisterFailCount >= MaxRegisterFailCount;
		}

		/// <summary>
		/// Зарегистрировать котируемую заявку.
		/// </summary>
		/// <param name="order">Котируемая заявка.</param>
		protected virtual void RegisterQuotingOrder(Order order)
		{
			AddOrderRules(order);

			if (_manualReRegisterOrder != null)
			{
				ReRegisterSlippage(_manualReRegisterOrder, order);
				_manualReRegisterOrder = null;
			}

			_isRegistering = true;
			RegisterOrder(order);
		}

		private void ResetRegisterFailCount()
		{
			this.AddInfoLog("Сброс счетчика ошибок регистрации с {0} до нуля.", CurrentRegisterFailCount);
			CurrentRegisterFailCount = 0;
		}

		private void ProcessRegisteredOrder(Order o)
		{
			if (o == _order)
			{
				_isRegistering = false;
				this.AddInfoLog("Заявка {0} принята биржей.", o.TransactionId);

				ResetRegisterFailCount();
			}
			else if (o == _reRegisteringOrder)
			{
				this.AddInfoLog("Заявка {0} перерегистрирована на заявку {1}.", _order.TransactionId, _reRegisteringOrder.TransactionId);

				Rules.RemoveRulesByToken(_order, null);

				_order = _reRegisteringOrder;
				_reRegisteringOrder = null;

				ResetRegisterFailCount();
			}
			else
				this.AddWarningLog("Заявка {0} устарела.", o.TransactionId);

			ProcessQuoting();
		}

		private void AddOrderRules(Order order)
		{
			var regRule = order
				.WhenRegistered()
				.Do(ProcessRegisteredOrder)
				.Once()
				.Sync(_syncToken)
				.Apply(this);

			var regFailRule = order
				.WhenRegisterFailed()
				.Do(fail =>
				{
					var o = fail.Order;

					this.AddErrorLog("Заявка {0} не принята биржей по причине '{1}'.", o.TransactionId, fail.Error.Message);

					var canProcess = false;

					if (o == _order)
					{
						_order = null;
						_isRegistering = false;
						canProcess = true;
					}
					else if (o == _reRegisteringOrder)
					{
						_reRegisteringOrder = null;
						_isReRegistedFailed = true;
					}
					else
						this.AddWarningLog("Заявка {0} устарела.", o.TransactionId);

					if (NeedStop(fail))
						Stop();
					else if (canProcess)
						ProcessQuoting();
				})
				.Once()
				.Sync(_syncToken)
				.Apply(this);

			regRule.Exclusive(regFailRule);

			var matchedRule = order
				.WhenMatched()
				.Do((r, o) =>
				{
					this.AddInfoLog("Заявка {0} полностью исполнилась. Оставшийся объем {1}.", o.TransactionId, LeftVolume);

					Rules.RemoveRulesByToken(o, r);

					// исполнилась заявка, которая сейчас в процессе регистрации
					if (o == _reRegisteringOrder)
					{
						ProcessRegisteredOrder(o);
					}

					// http://stocksharp.com/forum/yaf_postst1708_MarketQuotingStrategy---Obiem-zaiavki-nie-mozhiet-byt--nulievym.aspx
					if (NeedFinish())
					{
						this.AddInfoLog("Заканчиваем котирование.");
						Stop();
					}
					else
					{
						if (_order == o)
						{
							_manualReRegisterOrder = null;
							_isCanceling = false;
							_order = null;
							_reRegisteringOrder = null;
							_isReRegistedFailed = false;
							ProcessQuoting();
						}
						else
							this.AddWarningLog("Заявка {0} устарела.", o.TransactionId);
					}
				})
				.Once()
				.Sync(_syncToken)
				.Apply(this);

			regFailRule.Exclusive(matchedRule);
		}

		/// <summary>
		/// Инициировать котирование.
		/// </summary>
		protected virtual void ProcessQuoting()
		{
			if (ProcessState != ProcessStates.Started)
			{
				this.AddWarningLog("Котирование в состоянии {0}.", ProcessState);
				return;
			}

			if (_order != null)
			{
				if (_isCanceling)
				{
					this.AddWarningLog("Заявка {0} в процессе снятия.", _order.TransactionId);
					return;
				}
				else if (_isRegistering)
				{
					this.AddWarningLog("Заявка {0} в процессе регистрации.", _order.TransactionId);
					return;
				}
				else if (_reRegisteringOrder != null)
				{
					this.AddWarningLog("Заявка {0} в процессе перерегистрации на заявку {1}.", _order.TransactionId, _reRegisteringOrder.TransactionId);
					return;
				}
				else if (_isReRegistedFailed)
				{
					this.AddWarningLog("Перерегистрация не была выполнена. Ожидание исполнения заявки {0}.", _order.TransactionId);
					return;
				}

				// TODO: mika. Хак. Надо как-то под другому
				PositionManager.ProcessOrder(_order);

				if (ProcessState != ProcessStates.Started)
					return;
			}

			// pyhta4og: При тестировании на истории - нормальная ситуация что нет стакана или последней сделки в начале тестирования.
			//http://stocksharp.com/forum/yaf_postst779_Oshibka-zashchitnykh-stratieghii---kolliektsiia-kotirovok-pusta.aspx
			var priceRange = GetAcceptablePriceRange();
			if (priceRange == null)
			{
				this.AddWarningLog("Стакан пустой.");
				return;
			}

			//newPrice = Security.ShrinkPrice(newPrice);

			//if (CurrentBestPrice == null)
			//	CurrentBestPrice = newPrice;

			var newVolume = GetNewVolume();

			var newPrice = NeedQuoting(_order != null ? _order.Price : 0, _order != null ? _order.Balance : 0, priceRange, newVolume);
			if (newPrice == 0)
				return;

			newPrice = Security.ShrinkPrice(newPrice);
			//CurrentBestPrice = newPrice;

			this.AddInfoLog("Цена текущей {0} и лучшей {1}.", _order != null ? _order.Price : (object)"NULL", newPrice);
			this.AddInfoLog("Лучший бид {0} и лучший аск {1}.",
			                Security.BestBid == null ? (object)"NULL" : Security.BestBid.Price,
							Security.BestAsk == null ? (object)"NULL" : Security.BestAsk.Price);

			if (_order == null)
			{
				if (_manualReRegisterOrder != null && !CheckReRegisterLimits())
					return;

				if (!CheckIntervalLimit())
					return;
					
				_order = this.CreateOrder(QuotingDirection, newPrice, newVolume);
				RegisterQuotingOrder(_order);
			}
			else
			{
				this.AddInfoLog("Котирование заявки {0} на {1} с ценой {2} объемом {3}.", _order.TransactionId, _order.Direction, _order.Price, _order.Volume);

				if (Security.ExchangeBoard.IsSupportAtomicReRegister && Trader.IsSupportAtomicReRegister)
				{
					if (!CheckReRegisterLimits())
						return;

					if (!CheckIntervalLimit())
						return;

					var newOrder = _order.ReRegisterClone(newPrice);
						
					// биржа сама заполнит оставшуюсю часть
					newOrder.Volume = 0;

					AddOrderRules(newOrder);

					_reRegisteringOrder = newOrder;
					_isReRegistedFailed = false;
					ReRegisterOrder(_order, newOrder);

					this.AddInfoLog("Перекотирование зарегистрировано для заявки {0} на {1} с ценой {2}.", newOrder.TransactionId, newOrder.Direction, newOrder.Price);
				}
				else
				{
					this.AddInfoLog("Отмена заявки {0}.", _order.TransactionId);
					_isCanceling = true;

					_order
						.WhenCanceled()
						.Do((r, o) =>
						{
							this.AddInfoLog("Заявка {0} была снята. Время снятия {1}.", o.TransactionId, o.LastChangeTime);

							Rules.RemoveRulesByToken(o, r);

							if (_order == o)
							{
								_manualReRegisterOrder = _order;
								_order = null;
								_isCanceling = false;
								ProcessQuoting();
							}
							else
								this.AddWarningLog("Заявка {0} устарела.", o.TransactionId);
						})
						.Once()
						.Sync(_syncToken)
						.Apply(this);

					CancelOrder(_order);
				}
			}
		}

		private bool CheckReRegisterLimits()
		{
			if (CurrentReRegisterCount >= MaxReRegisterCount)
			{
				this.AddWarningLog("Невозможно перекотировать. Было совершено максимальное количество {0} попыток.", MaxReRegisterCount);
				Stop();
				return false;
			}

			CurrentReRegisterCount++;

			return true;
		}

		private bool CheckIntervalLimit()
		{
			if (RegisterInterval == default(TimeSpan))
				return true;

			var now = this.GetMarketTime();
			var diff = (_lastRegisterTime + RegisterInterval) - now;

			if (diff >= TimeSpan.Zero)
			{
				this.AddInfoLog("Заявка не может быть перерегистрирована, так как еще не истек интервал. " +
				                "Осталось {0}, начало {1}, текущее {2}, интервал {3}.", diff, _lastRegisterTime, now, RegisterInterval);

				return false;
			}
			else
			{
				_lastRegisterTime = now;
				return true;
			}
		}
	}
}